# Copyright 2003, 2005 Dave Abrahams
# Copyright 2005, 2006 Rene Rivera
# Copyright 2005 Toon Knapen
# Copyright 2002, 2003, 2004, 2005, 2006 Vladimir Prus
# Distributed under the Boost Software License, Version 1.0.
# (See accompanying file LICENSE.txt or https://www.bfgroup.xyz/b2/LICENSE.txt)

#   Provides actions common to all toolsets, such as creating directories and
# removing files.

import os ;
import modules ;
import utility ;
import print ;
import type ;
import feature ;
import errors ;
import path ;
import sequence ;
import toolset ;
import virtual-target ;
import numbers ;

if [ MATCH (--debug-configuration) : [ modules.peek : ARGV ] ]
{
    .debug-configuration = true ;
}
if [ MATCH (--show-configuration) : [ modules.peek : ARGV ] ]
{
    .show-configuration = true ;
}

# Configurations
#
# The following class helps to manage toolset configurations. Each configuration
# has a unique ID and one or more parameters. A typical example of a unique ID
# is a condition generated by 'common.check-init-parameters' rule. Other kinds
# of IDs can be used. Parameters may include any details about the configuration
# like 'command', 'path', etc.
#
# A toolset configuration may be in one of the following states:
#
#   - registered
#       Configuration has been registered (e.g. explicitly or by auto-detection
#       code) but has not yet been marked as used, i.e. 'toolset.using' rule has
#       not yet been called for it.
#   - used
#       Once called 'toolset.using' rule marks the configuration as 'used'.
#
# The main difference between the states above is that while a configuration is
# 'registered' its options can be freely changed. This is useful in particular
# for autodetection code - all detected configurations may be safely overwritten
# by user code.

class configurations
{
    import errors ;

    rule __init__ ( )
    {
    }

    # Registers a configuration.
    #
    # Returns 'true' if the configuration has been added and an empty value if
    # it already exists. Reports an error if the configuration is 'used'.
    #
    rule register ( id )
    {
        if $(id) in $(self.used)
        {
            errors.error "common: the configuration '$(id)' is in use" ;
        }

        local retval ;

        if ! $(id) in $(self.all)
        {
            self.all += $(id) ;

            # Indicate that a new configuration has been added.
            retval = true ;
        }

        return $(retval) ;
    }

    # Mark a configuration as 'used'.
    #
    # Returns 'true' if the state of the configuration has been changed to
    # 'used' and an empty value if it the state has not been changed. Reports an
    # error if the configuration is not known.
    #
    rule use ( id )
    {
        if ! $(id) in $(self.all)
        {
            errors.error "common: the configuration '$(id)' is not known" ;
        }

        local retval ;

        if ! $(id) in $(self.used)
        {
            self.used += $(id) ;

            # Indicate that the configuration has been marked as 'used'.
            retval = true ;
        }

        return $(retval) ;
    }

    # Return all registered configurations.
    #
    rule all ( )
    {
        return $(self.all) ;
    }

    # Return all used configurations.
    #
    rule used ( )
    {
        return $(self.used) ;
    }

    # Returns the value of a configuration parameter.
    #
    rule get ( id : param )
    {
        return $(self.$(param).$(id)) ;
    }

    # Sets the value of a configuration parameter.
    #
    rule set ( id : param : value * )
    {
        self.$(param).$(id) = $(value) ;
    }
}


# The rule for checking toolset parameters. Trailing parameters should all be
# parameter name/value pairs. The rule will check that each parameter either has
# a value in each invocation or has no value in each invocation. Also, the rule
# will check that the combination of all parameter values is unique in all
# invocations.
#
# Each parameter name corresponds to a subfeature. This rule will declare a
# subfeature the first time a non-empty parameter value is passed and will
# extend it with all the values.
#
# The return value from this rule is a condition to be used for flags settings.
#
rule check-init-parameters ( toolset requirement * : * )
{
    local sig = $(toolset) ;
    local condition = <toolset>$(toolset) ;
    local subcondition ;
    for local index in 2 3 4 5 6 7 8 9
    {
        local name = $($(index)[1]) ;
        local value = $($(index)[2]) ;

        if $(value)-is-not-empty
        {
            condition = $(condition)-$(value) ;
            if $(.had-unspecified-value.$(toolset).$(name))
            {
                errors.user-error
                    "$(toolset) initialization: parameter '$(name)'"
                    "inconsistent" : "no value was specified in earlier"
                    "initialization" : "an explicit value is specified now" ;
            }
            # The below logic is for intel compiler. It calls this rule with
            # 'intel-linux' and 'intel-win' as toolset, so we need to get the
            # base part of toolset name. We can not pass 'intel' as toolset
            # because in that case it will be impossible to register versionless
            # intel-linux and intel-win toolsets of a specific version.
            local t = $(toolset) ;
            local m = [ MATCH "([^-]*)-" : $(toolset) ] ;
            if $(m)
            {
                t = $(m[1]) ;
            }
            if ! $(.had-value.$(toolset).$(name))
            {
                if ! $(.declared-subfeature.$(t).$(name))
                {
                    feature.subfeature toolset $(t) : $(name) : : propagated ;
                    .declared-subfeature.$(t).$(name) = true ;
                }
                .had-value.$(toolset).$(name) = true ;
            }
            feature.extend-subfeature toolset $(t) : $(name) : $(value) ;
            subcondition += <toolset-$(t):$(name)>$(value) ;
        }
        else
        {
            if $(.had-value.$(toolset).$(name))
            {
                errors.user-error
                    "$(toolset) initialization: parameter '$(name)'"
                    "inconsistent" : "an explicit value was specified in an"
                    "earlier initialization" : "no value is specified now" ;
            }
            .had-unspecified-value.$(toolset).$(name) = true ;
        }
        sig = $(sig)$(value:E="")- ;
    }
    # We also need to consider requirements on the toolset as we can
    # configure the same toolset multiple times with different options that
    # are selected with the requirements.
    if $(requirement)
    {
        sig = $(sig)$(requirement:J=,) ;
    }
    if $(sig) in $(.all-signatures)
    {
        local message =
            "duplicate initialization of $(toolset) with the following parameters: " ;
        for local index in 2 3 4 5 6 7 8 9
        {
            local p = $($(index)) ;
            if $(p)
            {
                message += "$(p[1]) = $(p[2]:E=<unspecified>)" ;
            }
        }
        message += "previous initialization at $(.init-loc.$(sig))" ;
        errors.user-error
            $(message[1]) : $(message[2]) : $(message[3]) : $(message[4]) :
            $(message[5]) : $(message[6]) : $(message[7]) : $(message[8]) ;
    }
    .all-signatures += $(sig) ;
    .init-loc.$(sig) = [ errors.nearest-user-location ] ;

    # If we have a requirement, this version should only be applied under that
    # condition. To accomplish this we add a toolset requirement that imposes
    # the toolset subcondition, which encodes the version.
    if $(requirement)
    {
        local r = <toolset>$(toolset) $(requirement) ;
        r = $(r:J=,) ;
        toolset.add-requirements "$(r):$(subcondition)" ;
    }

    # We add the requirements, if any, to the condition to scope the toolset
    # variables and options to this specific version.
    condition += $(requirement) ;

    if $(.show-configuration)
    {
        ECHO "notice:" $(condition) ;
    }
    return $(condition:J=/) ;
}


# A helper rule to get the command to invoke some tool. If
# 'user-provided-command' is not given, tries to find binary named 'tool' in
# PATH and in the passed 'additional-path'. Otherwise, verifies that the first
# element of 'user-provided-command' is an existing program.
#
# This rule returns the command to be used when invoking the tool. If we can not
# find the tool, a warning is issued. If 'path-last' is specified, PATH is
# checked after 'additional-paths' when searching for 'tool'.
#
rule get-invocation-command-nodefault ( toolset : tool :
    user-provided-command * : additional-paths * : path-last ? )
{
    local command ;
    if ! $(user-provided-command)
    {
        command = [ find-tool $(tool) : $(additional-paths) : $(path-last) ] ;
        if ! $(command) && $(.debug-configuration)
        {
            ECHO "warning:" toolset $(toolset) "initialization:" can not find tool
                $(tool) ;
            ECHO "warning:" initialized from [ errors.nearest-user-location ] ;
        }
    }
    else
    {
        command = [ check-tool $(user-provided-command) ] ;
        if ! $(command) && $(.debug-configuration)
        {
            ECHO "warning:" toolset $(toolset) "initialization:" ;
            ECHO "warning:" can not find user-provided command
                '$(user-provided-command)' ;
            ECHO "warning:" initialized from [ errors.nearest-user-location ] ;
        }
    }

    return $(command) ;
}


# Same as get-invocation-command-nodefault, except that if no tool is found,
# returns either the user-provided-command, if present, or the 'tool' parameter.
#
rule get-invocation-command ( toolset : tool : user-provided-command * :
    additional-paths * : path-last ? )
{
    local result = [ get-invocation-command-nodefault $(toolset) : $(tool) :
        $(user-provided-command) : $(additional-paths) : $(path-last) ] ;

    if ! $(result)
    {
        if $(user-provided-command)
        {
            result = $(user-provided-command) ;
        }
        else
        {
            result = $(tool) ;
        }
    }
    return $(result) ;
}


# Given an invocation command return the absolute path to the command. This
# works even if command has no path element and was found on the PATH.
#
rule get-absolute-tool-path ( command )
{
    if $(command:D)
    {
        return $(command:D) ;
    }
    else
    {
        local m = [ GLOB [ modules.peek : PATH Path path ] : $(command)
            $(command).exe ] ;
        return $(m[1]:D) ;
    }
}


# Attempts to find tool (binary) named 'name' in PATH and in 'additional-paths'.
# If found in PATH, returns 'name' and if found in additional paths, returns
# absolute name. If the tool is found in several directories, returns the first
# path found. Otherwise, returns an empty string. If 'path-last' is specified,
# PATH is searched after 'additional-paths'.
#
rule find-tool ( name : additional-paths * : path-last ? )
{
    if $(name:D)
    {
        return [ check-tool-aux $(name) ] ;
    }
    local path = [ path.programs-path ] ;
    local match = [ path.glob $(path) : $(name) $(name).exe ] ;
    local additional-match = [ path.glob $(additional-paths) : $(name)
        $(name).exe ] ;

    local result ;
    if $(path-last)
    {
        result = $(additional-match) ;
        if ! $(result) && $(match)
        {
            result = $(name) ;
        }
    }
    else
    {
        if $(match)
        {
            result = $(name) ;
        }
        else
        {
            result = $(additional-match) ;
        }
    }
    if $(result)
    {
        return [ path.native $(result[1]) ] ;
    }
}

# Checks if 'command' can be found either in path or is a full name to an
# existing file.
#
local rule check-tool-aux ( command )
{
    if $(command:D)
    {
        if [ path.exists $(command) ]
            # Both NT and Cygwin will run .exe files by their unqualified names.
            || ( [ os.on-windows ] && [ path.exists $(command).exe ] )
            # Only NT will run .bat & .cmd files by their unqualified names.
            || ( ( [ os.name ] = NT ) && ( [ path.exists $(command).bat ] ||
                [ path.exists $(command).cmd ] ) )
        {
            return $(command) ;
        }
    }
    else
    {
        if [ GLOB [ modules.peek : PATH Path path ] : $(command) ]
        {
            return $(command) ;
        }
    }
}


# Checks that a tool can be invoked by 'command'. If command is not an absolute
# path, checks if it can be found in 'path'. If command is an absolute path,
# check that it exists. Returns 'command' if ok or empty string otherwise.
#
local rule check-tool ( xcommand + )
{
    if [ check-tool-aux $(xcommand[1]) ] ||
       [ check-tool-aux $(xcommand[-1]) ]
    {
        return $(xcommand) ;
    }
}


# Handle common options for toolset, specifically sets the following flag
# variables:
# - CONFIG_COMMAND to $(command)
# - OPTIONS for compile         to the value of <compileflags> in $(options)
# - OPTIONS for compile.c       to the value of <cflags>       in $(options)
# - OPTIONS for compile.c++     to the value of <cxxflags>     in $(options)
# - OPTIONS for compile.asm     to the value of <asmflags>     in $(options)
# - OPTIONS for compile.fortran to the value of <fflags>       in $(options)
# - OPTIONS for link            to the value of <linkflags>    in $(options)
#
rule handle-options ( toolset : condition * : command * : options * )
{
    if $(.debug-configuration)
    {
        ECHO "notice:" will use '$(command)' for $(toolset), condition
            $(condition:E=(empty)) ;
    }

    #   The last parameter ('unchecked') says it is OK to set flags for another
    # module.
    toolset.flags $(toolset) CONFIG_COMMAND $(condition) : $(command)
        : unchecked ;

    toolset.flags $(toolset).compile         OPTIONS $(condition) :
        [ feature.get-values <compileflags> : $(options) ] : unchecked ;

    toolset.flags $(toolset).compile.c       OPTIONS $(condition) :
        [ feature.get-values <cflags>       : $(options) ] : unchecked ;

    toolset.flags $(toolset).compile.c++     OPTIONS $(condition) :
        [ feature.get-values <cxxflags>     : $(options) ] : unchecked ;

    toolset.flags $(toolset).compile.asm     OPTIONS $(condition) :
        [ feature.get-values <asmflags>     : $(options) ] : unchecked ;

    toolset.flags $(toolset).compile.fortran OPTIONS $(condition) :
        [ feature.get-values <fflags>       : $(options) ] : unchecked ;

    toolset.flags $(toolset).link            OPTIONS $(condition) :
        [ feature.get-values <linkflags>    : $(options) ] : unchecked ;
}


# Returns the location of the "program files" directory on a Windows platform.
#
rule get-program-files-dir ( )
{
    local ProgramFiles = [ modules.peek : ProgramFiles ] ;
    if $(ProgramFiles)
    {
        ProgramFiles = "$(ProgramFiles:J= )" ;
    }
    else
    {
        ProgramFiles = "c:\\Program Files" ;
    }
    return $(ProgramFiles) ;
}


if [ os.name ] = NT
{
    NULL_DEVICE = "NUL" ;
    IGNORE = "2>$(NULL_DEVICE) >$(NULL_DEVICE) & setlocal" ;
    RM = del /f /q ;
    CP = copy /b ;
    LN ?= $(CP) ;
    # Ugly hack to convince copy to set the timestamp of the destination to the
    # current time by concatenating the source with a nonexistent file. Note
    # that this requires /b (binary) as the default when concatenating files is
    # /a (ascii).
    WINDOWS-CP-HACK = "+ this-file-does-not-exist-A698EE7806899E69" ;
}
else if [ os.name ] = VMS
{
    NULL_DEVICE = "NL:" ;
    PIPE = PIPE ;
    IGNORE = "2>$(NULL_DEVICE) >$(NULL_DEVICE)" ;
    RM = DELETE /NOCONF ;
    CP = COPY /OVERWRITE ;
    LN = $(CP) ;
}
else
{
    NULL_DEVICE = "/dev/null" ;
    IGNORE = "2>$(NULL_DEVICE) >$(NULL_DEVICE)" ;
    RM = rm -f ;
    CP = cp ;
    LN = ln ;
}

NULL_OUT = ">$(NULL_DEVICE)" ;

rule null-device ( )
{
    return $(NULL_DEVICE) ;
}


rule rm-command ( )
{
    return $(RM) ;
}


rule copy-command ( )
{
    return $(CP) ;
}


if "\n" = "n"
{
    # Escape characters not supported so use ugly hacks. Will not work on Cygwin
    # - see below.
    nl = "
" ;
    q = "" ;
}
else
{
    nl = "\n" ;
    q = "\"" ;
}


rule newline-char ( )
{
    return $(nl) ;
}


# Returns the command needed to set an environment variable on the current
# platform. The variable setting persists through all following commands and is
# visible in the environment seen by subsequently executed commands. In other
# words, on Unix systems, the variable is exported, which is consistent with the
# only possible behavior on Windows systems.
#
rule variable-setting-command ( variable : value )
{
    if [ os.name ] = NT
    {
        return "set $(variable)=$(value)$(nl)" ;
    }
    else if [ os.name ] = VMS
    {
        return "$(variable) == $(q)$(value)$(q)$(nl)" ;
    }
    else
    {
        # If we do not have escape character support in bjam, the cod below
        # blows up on CYGWIN, since the $(nl) variable holds a Windows new-line
        # \r\n sequence that messes up the executed export command which then
        # reports that the passed variable name is incorrect.
        # But we have a check for cygwin in kernel/bootstrap.jam already.
        return "$(variable)=$(q)$(value)$(q)$(nl)export $(variable)$(nl)" ;
    }
}


# Returns a command to sets a named shell path variable to the given NATIVE
# paths on the current platform.
#
rule path-variable-setting-command ( variable : paths * )
{
    local sep = [ os.path-separator ] ;
    return [ variable-setting-command $(variable) : $(paths:J=$(sep)) ] ;
}


# Returns a command that prepends the given paths to the named path variable on
# the current platform.
#
rule prepend-path-variable-command ( variable : paths * )
{
    return [ path-variable-setting-command $(variable)
        : $(paths) [ os.expand-variable $(variable) ] ] ;
}


# Return a command which can create a file. If 'r' is result of invocation, then
# 'r foobar' will create foobar with unspecified content. What happens if file
# already exists is unspecified.
#
rule file-creation-command ( )
{
    if [ os.name ] = NT
    {
        # A few alternative implementations on Windows:
        #
        #   'type NUL >> '
        #        That would construct an empty file instead of a file containing
        #      a space and an end-of-line marker but it would also not change
        #      the target's timestamp in case the file already exists.
        #
        #   'type NUL > '
        #        That would construct an empty file instead of a file containing
        #      a space and an end-of-line marker but it would also destroy an
        #      already existing file by overwriting it with an empty one.
        #
        #   I guess the best solution would be to allow Boost Jam to define
        # built-in functions such as 'create a file', 'touch a file' or 'copy a
        # file' which could be used from inside action code. That would allow
        # completely portable operations without this kind of kludge.
        #                                            (22.02.2009.) (Jurko)
        return "echo. > " ;
    }
    else if [ os.name ] = VMS
    {
        return "APPEND /NEW NL: " ;
    }
    else
    {
        return "touch " ;
    }
}


# Returns a command that may be used for 'touching' files. It is not a real
# 'touch' command on NT because it adds an empty line at the end of file but it
# works with source files.
#
rule file-touch-command ( )
{
    if [ os.name ] = NT
    {
        return "echo. >> " ;
    }
    else if [ os.name ] = VMS
    {
        return "APPEND /NEW NL: " ;
    }
    else
    {
        return "touch " ;
    }
}


rule MkDir
{
    # If dir exists, do not update it. Do this even for $(DOT).
    NOUPDATE $(<) ;

    if $(<) != $(DOT) && ! $($(<)-mkdir)
    {
        # Cheesy gate to prevent multiple invocations on same dir.
        $(<)-mkdir = true ;

        # Schedule the mkdir build action.
        common.mkdir $(<) ;

        # Prepare a Jam 'dirs' target that can be used to make the build only
        # construct all the target directories.
        DEPENDS dirs : $(<) ;

        # Recursively create parent directories. $(<:P) = $(<)'s parent & we
        # recurse until root.

        local s = $(<:P) ;
        if [ os.name ] = NT
        {
            switch $(s)
            {
                case "*:"   : s = ;
                case "*:\\" : s = ;
            }
        }

        if $(s)
        {
            if $(s) != $(<)
            {
                DEPENDS $(<) : $(s) ;
                MkDir $(s) ;
            }
            else
            {
                NOTFILE $(s) ;
            }
        }
    }
}


#actions MkDir1
#{
#    mkdir "$(<)"
#}

#   The following quick-fix actions should be replaced using the original MkDir1
# action once Boost Jam gets updated to correctly detect different paths leading
# up to the same filesystem target and triggers their build action only once.
#                                             (todo) (04.07.2008.) (Jurko)

if [ os.name ] = NT
{
    actions quietly mkdir
    {
        if not exist "$(<)\\" mkdir "$(<)"
    }
}
else
{
    actions quietly mkdir
    {
        mkdir -p "$(<)"
    }
}


actions piecemeal together existing Clean
{
    $(RM) "$(>)"
}


rule copy
{
}


actions copy
{
    $(CP) "$(>)" $(WINDOWS-CP-HACK) "$(<)"
}


rule RmTemps
{
}


actions quietly updated piecemeal together RmTemps
{
    $(RM) "$(>)" $(IGNORE)
}


actions hard-link
{
    $(RM) "$(<)" 2$(NULL_OUT) $(NULL_OUT)
    $(LN) "$(>)" "$(<)" $(NULL_OUT)
}


if [ os.name ] = VMS
{
    actions mkdir
    {
        IF F$PARSE("$(<:W)") .EQS. "" THEN CREATE /DIR $(<:W)
    }

    actions piecemeal together existing Clean
    {
        $(RM) $(>:WJ=;*,);*
    }

    actions copy
    {
        $(CP) $(>:WJ=,) $(<:W)
    }

    actions quietly updated piecemeal together RmTemps
    {
        $(PIPE) $(RM) $(>:WJ=;*,);* $(IGNORE)
    }

    actions hard-link
    {
        $(PIPE) $(RM) $(>[1]:W);* $(IGNORE)
        $(PIPE) $(LN) $(>[1]:W) $(<:W) $(NULL_OUT)
    }
}

# Given a target, as given to a custom tag rule, returns a string formatted
# according to the passed format. Format is a list of properties that is
# represented in the result. For each element of format the corresponding target
# information is obtained and added to the result string. For all, but the
# literal, the format value is taken as the as string to prepend to the output
# to join the item to the rest of the result. If not given "-" is used as a
# joiner.
#
# The format options can be:
#
#   <base>[joiner]
#       ::  The basename of the target name.
#   <toolset>[joiner]
#       ::  The abbreviated toolset tag being used to build the target.
#   <threading>[joiner]
#       ::  Indication of a multi-threaded build.
#   <runtime>[joiner]
#       ::  Collective tag of the build runtime.
#   <version:/version-feature | X.Y[.Z]/>[joiner]
#       ::  Short version tag taken from the given "version-feature" in the
#           build properties. Or if not present, the literal value as the
#           version number.
#   <property:/property-name/>[joiner]
#       ::  Direct lookup of the given property-name value in the build
#           properties. /property-name/ is a regular expression. E.g.
#           <property:toolset-.*:flavor> will match every toolset.
#   /otherwise/
#       ::  The literal value of the format argument.
#
# For example this format:
#
#   boost_ <base> <toolset> <threading> <runtime> <version:boost-version>
#
# Might return:
#
#   boost_thread-vc80-mt-gd-1_33.dll, or
#   boost_regex-vc80-gd-1_33.dll
#
# The returned name also has the target type specific prefix and suffix which
# puts it in a ready form to use as the value from a custom tag rule.
#
rule format-name ( format * : name : type ? : property-set )
{
    local result = "" ;
    for local f in $(format)
    {
        switch $(f:G)
        {
            case <base> :
                result += $(name:B) ;

            case <toolset> :
                result += [ join-tag $(f:G=) : [ toolset-tag $(name) : $(type) :
                    $(property-set) ] ] ;

            case <threading> :
                result += [ join-tag $(f:G=) : [ threading-tag $(name) : $(type)
                    : $(property-set) ] ] ;

            case <runtime> :
                result += [ join-tag $(f:G=) : [ runtime-tag $(name) : $(type) :
                    $(property-set) ] ] ;

            case <qt> :
                result += [ join-tag $(f:G=) : [ qt-tag $(name) : $(type) :
                    $(property-set) ] ] ;

            case <address-model> :
                result += [ join-tag $(f:G=) : [ address-model-tag $(name) :
                    $(type) : $(property-set) ] ] ;

            case <arch-and-model> :
                result += [ join-tag $(f:G=) : [ arch-and-model-tag $(name) :
                    $(type) : $(property-set) ] ] ;

            case <version:*> :
                local key = [ MATCH <version:(.*)> : $(f:G) ] ;
                local version = [ $(property-set).get <$(key)> ] ;
                version ?= $(key) ;
                version = [ MATCH "^([^.]+)[.]([^.]+)[.]?([^.]*)" : $(version) ] ;
                result += [ join-tag $(f:G=) : $(version[1])_$(version[2]) ] ;

            case <property:*> :
                local key = [ MATCH <property:(.*)> : $(f:G) ] ;
                local p0 = [ MATCH <($(key))> : [ $(property-set).raw ] ] ;
                if $(p0)
                {
                    local p = [ $(property-set).get <$(p0)> ] ;
                    if $(p)
                    {
                        result += [ join-tag $(f:G=) : $(p) ] ;
                    }
                }

            case * :
                result += $(f:G=) ;
        }
    }
    return [ virtual-target.add-prefix-and-suffix $(result:J=) : $(type) :
        $(property-set) ] ;
}


local rule join-tag ( joiner ? : tag ? )
{
    if ! $(joiner) { joiner = - ; }
    return $(joiner)$(tag) ;
}


local rule toolset-tag ( name : type ? : property-set )
{
    local tag = ;

    local properties = [ $(property-set).raw ] ;
    switch [ $(property-set).get <toolset> ]
    {
        case borland* : tag += bcb ;
        case clang* :
        {
            switch [ $(property-set).get <toolset-clang:platform> ]
            {
               case darwin : tag += clang-darwin ;
               case linux  : tag += clang ;
               case win    : tag += clangw ;
            }
        }
        case como* : tag += como ;
        case cw : tag += cw ;
        case darwin* : tag += xgcc ;
        case edg* : tag += edg ;
        case embarcadero* : tag += embtc ;
        case gcc* :
        {
            switch [ $(property-set).get <target-os> ]
            {
                case *windows* : tag += mgw ;
                case * : tag += gcc ;
            }
        }
        case intel :
        if [ $(property-set).get <toolset-intel:platform> ] = win
        {
            tag += iw ;
        }
        else
        {
            tag += il ;
        }
        case kcc* : tag += kcc ;
        case kylix* : tag += bck ;
        #case metrowerks* : tag += cw ;
        #case mingw* : tag += mgw ;
        case mipspro* : tag += mp ;
        case msvc* : tag += vc ;
        case qcc* : tag += qcc ;
        case sun* : tag += sw ;
        case tru64cxx* : tag += tru ;
        case vacpp* : tag += xlc ;
    }
    local version = [ MATCH "<toolset.*version>([0123456789]+)[.]?([0123456789]*)"
        : $(properties) ] ;
    # For historical reasons, vc6.0 and vc7.0 use different naming.
    if $(tag) = vc
    {
        if $(version[1]) = 6
        {
            # Cancel minor version.
            version = 6 ;
        }
        else if $(version[1]) = 7 && $(version[2]) = 0
        {
            version = 7 ;
        }
    }

    # From GCC 5, versioning changes and minor becomes patch
    if ( $(tag) = gcc || $(tag) = mgw ) && $(version[1]) && [ numbers.less 4 $(version[1]) ]
    {
        version = $(version[1]) ;
    }

    # Ditto, from Clang 4
    if ( $(tag) = clang || $(tag) = clangw ) && $(version[1]) && [ numbers.less 3 $(version[1]) ]
    {
        version = $(version[1]) ;
    }

    # On intel, version is not added, because it does not matter and it is the
    # version of vc used as backend that matters. Ideally, we should encode the
    # backend version but that would break compatibility with V1.
    if $(tag) = iw
    {
        version = ;
    }

    # On borland, version is not added for compatibility with V1.
    if $(tag) = bcb
    {
        version = ;
    }

    # Use un-versioned toolset name for Embarcadero, to avoid having to update
    # the auto-linking logic on every release.
    if $(tag) = embtc
    {
        version = ;
    }

    tag += $(version) ;

    return $(tag:J=) ;
}


local rule threading-tag ( name : type ? : property-set )
{
    if <threading>multi in [ $(property-set).raw ]
    {
        return mt ;
    }
}


local rule runtime-tag ( name : type ? : property-set )
{
    local tag = ;

    local properties = [ $(property-set).raw ] ;
    if <runtime-link>static in $(properties) { tag += s ; }

    # This is an ugly thing. In V1, there is code to automatically detect which
    # properties affect a target. So, if <runtime-debugging> does not affect gcc
    # toolset, the tag rules will not even see <runtime-debugging>. Similar
    # functionality in V2 is not implemented yet, so we just check for toolsets
    # known to care about runtime debugging.
    if ( <toolset>msvc in $(properties) ) ||
        ( <stdlib>stlport in $(properties) ) ||
        ( <toolset-intel:platform>win in $(properties) ) ||
        ( <toolset-clang:platform>win in $(properties) )
    {
        if <runtime-debugging>on in $(properties) { tag += g ; }
    }

    if <python-debugging>on in $(properties) { tag += y ; }
    if <variant>debug in $(properties) { tag += d ; }
    if <stdlib>stlport in $(properties) { tag += p ; }
    if <stdlib-stlport:iostream>hostios in $(properties) { tag += n ; }

    return $(tag:J=) ;
}


# Create a tag for the Qt library version
# "<qt>4.6.0" will result in tag "qt460"
local rule qt-tag ( name : type ? : property-set )
{
    local v = [ MATCH "([0123456789]+)[.]?([0123456789]*)[.]?([0123456789]*)" :
        [ $(property-set).get <qt> ] ] ;
    return qt$(v:J=) ;
}


# Create a tag for the address-model
# <address-model>64 will simply generate "64"
local rule address-model-tag ( name : type ? : property-set )
{
    return [ $(property-set).get <address-model> ] ;
}

# Create a tag for the architecture and model
# <architecture>x86 <address-model>32 would generate "x32"
# This relies on the fact that all architectures start with
# unique letters.
local rule arch-and-model-tag ( name : type ? : property-set )
{
    local architecture = [ $(property-set).get <architecture> ] ;
    local address-model = [ $(property-set).get <address-model> ] ;

    local arch = [ MATCH ^(.) : $(architecture) ] ;

    return $(arch)$(address-model) ;
}

# TODO: probably needs to escape '"' in command parts
# TODO: if part does not contain whitespaces it does not require escaping
rule make-command-string ( command + )
{
    local command-string = \"$(command)\" ;
    return $(command-string:J=" ") ;
}

rule find-compiler ( toolset : tool : version ? : command * : additional-paths * )
{
    #1): use user-provided command
    if $(command)
    {
        if ! [ get-invocation-command-nodefault $(toolset) : $(tool)
             : $(command) : $(additional-paths) ]
        {
            local command-string = [ make-command-string $(command) ] ;
            errors.error toolset $(toolset) "initialization:"
                : provided command '$(command-string)' not found
                : initialized from [ errors.nearest-user-location ] ;
        }
    }
    #2): enforce user-provided version
    else if $(version)
    {
        command = [ get-invocation-command-nodefault $(toolset) : $(tool)-$(version)
                  : : $(additional-paths) ] ;

        #2.1) fallback: check whether "$(tool)" reports the requested version
        if ! $(command) { # ?= operator does not short-circuit
        command ?= [ get-invocation-command-nodefault $(toolset) : $(tool)
                   : : $(additional-paths) ] ;
        }

        if ! $(command)
        {
            errors.error toolset $(toolset) "initialization:"
                : version '$(version)' requested but neither
                    '$(tool)-$(version)' nor default '$(tool)' found
                : initialized from [ errors.nearest-user-location ] ;
        }

        import $(toolset) ;
        local tool-version = [ $(toolset).get-full-version $(command) ] ;

        import version ;
        if ! [ version.version-compatible [ SPLIT_BY_CHARACTERS $(version) : . ]
                                        : [ SPLIT_BY_CHARACTERS $(tool-version) : . ] ]
        {
            errors.error toolset $(toolset) "initialization:"
                : version '$(version)' requested but
                    '$(tool)-$(version)' not found and version
                    '$(tool-version:J=.)' of default '$(command)'
                    does not match
                : initialized from [ errors.nearest-user-location ]
                ;
        }
    }
    #3) default: no command and no version specified, try using "$(tool)"
    else
    {
        command = [ get-invocation-command-nodefault $(toolset) : $(tool)
                  : : $(additional-paths) ] ;
        if ! $(command)
        {
            errors.error toolset $(toolset) "initialization:"
                : no command provided, default command '$(tool)' not found
                : initialized from [ errors.nearest-user-location ] ;
        }
    }
    return $(command) ;
}

rule match-command-output ( kind ? : pattern : command-string )
{
    local output = [ SHELL $(command-string) : exit-status ] ;
    if 0 != $(output[2])
    {
        errors.error '$(command-string)'
                     exited with error code $(output[2]) ;
    }

    local match = [ MATCH $(pattern) : $(output[1]) ] ;
    if ! $(match)
    {
        errors.error '$(command-string)'
                     returned an invalid $kind string '$(output[1])' ;
    }

    return $(match) ;
}

rule __test__ ( )
{
    import assert ;

    local save-os = [ modules.peek os : .name ] ;

    modules.poke os : .name : LINUX ;
    assert.result "PATH=\"foo:bar:baz\"\nexport PATH\n"
        : path-variable-setting-command PATH : foo bar baz ;
    assert.result "PATH=\"foo:bar:$PATH\"\nexport PATH\n"
        : prepend-path-variable-command PATH : foo bar ;

    modules.poke os : .name : NT ;
    assert.result "set PATH=foo;bar;baz\n"
        : path-variable-setting-command PATH : foo bar baz ;
    assert.result "set PATH=foo;bar;%PATH%\n"
        : prepend-path-variable-command PATH : foo bar ;

    modules.poke os : .name : $(save-os) ;
}
